package core.reports;

import static org.junit.Assert.*;

import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Random;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import persistence.DataPersistor;

import common.TimeSource;
import common.util.DateUtils;

import core.*;
import core.exception.ModelException;
import core.exception.NotFoundException;

/*****************************************
 * Testing 
 *  3, 
 *  4, 
 *  5b, 
 *  9b, 
 *  10e-h, 
 *  11b, 
 *  13d-f
 * @author Sabrina
 *
 */
public class SabrinaProductStatsTest 
{
	private static Random _rand;
	private ContainerFacade _cFacade;
	private ItemFacade _iFacade;
	private ProductFacade _pFacade;
	private Calendar _now;
	private Container _su;
	private Product _prod1;
	private Product _prod2;
	private Product _prod3;
	
	@BeforeClass
	public static void setUpBeforeClass() throws Exception 
	{
		_rand = new Random();
	}

	@AfterClass
	public static void tearDownAfterClass() throws Exception {
	}

	@Before
	public void setUp() throws Exception 
	{
		DataPersistor.instance().init("-serial");
		ContainerFacade.clearAllData();
		ItemFacade.clearAllData();
		ProductFacade.clearAllData();
		_cFacade = ContainerFacade.instance();
		_iFacade = ItemFacade.instance();
		_pFacade = ProductFacade.instance();
		TimeSource.instance().reset();
		_now = Calendar.getInstance();
		_su = _cFacade.addStorageUnit("mySU");
		_prod1 = new Product("1","1stProd", "5", "grams", 0, 0);	
		_prod2 = new Product ("221234567890123456789022", "2ndProd", "1", "count", 1, 2);
		_prod3 = new Product ("333333333333", "3rdProd", "1", "fluid ounces", 1, 2);
	}
	
	@After
	public void tearDown() throws Exception {
	}

	@Test
	//////////////////////////////////////////////////////////////////////
	// Test 3a, b, c and 13 d, e, f
	// 3a.The product was added in the middle of the reporting period
	// 3b. The product was added on the first day of the reporting period
	// 3c. The product was added on the last day of the reporting period
	// 9b.  Test for 1 month
	// 11b. Test with 3 products
	// 13d.  Short barcode
	// 13e. Long barcode
	// 13f. Regular barcode
	///////////////////////////////////////////////////////////////////////
	public void test3abc9b11b13def() throws ModelException, IOException 
	{
		int months = 1;
		Calendar first = (Calendar) _now.clone();
		first.add(Calendar.MONTH, -1*months);
		Calendar middle = (Calendar) _now.clone();
		int randomNum = _rand.nextInt(28) + 1;
		middle.add(Calendar.DAY_OF_MONTH, -1*randomNum);
		Calendar last = (Calendar)_now.clone();
		if (last.get(Calendar.HOUR_OF_DAY) < 1)
			last.add(Calendar.HOUR_OF_DAY, -1);
		
		//a setup
		TimeSource.instance().setTime(middle);
		_pFacade.addProduct(_prod2);
		_iFacade.addItem(_prod2, _su);
		
		//b setup
		TimeSource.instance().setTime(first);
		_pFacade.addProduct(_prod1);
		_iFacade.addItem(_prod1, _su);
		
		//c setup
		TimeSource.instance().setTime(last);
		_pFacade.addProduct(_prod3);
		_iFacade.addItem(_prod3, _su);
		
		TimeSource.instance().setTime(_now);
		List<List<String>> data = runReport(months);
		testAssert(1, _prod1, data, months);
		testAssert(2, _prod2, data, months);
		testAssert(3, _prod3, data, months);
	}
	
	@Test
	/////////////////////////////////////////////////////////////////////
	// 4a. The product was added many days before the reporting period
	// 4b. The product was added the day before the reporting period
	// 5b. Test running the report on the 30th of a month
	////////////////////////////////////////////////////////////////////
	public void test4ab5b() throws ModelException, IOException
	{
		_now.set(Calendar.DAY_OF_MONTH, 30);
		
		if (_now.get(Calendar.DAY_OF_MONTH) != 30)
		{
			_now.set(Calendar.DAY_OF_MONTH, 30);
		}
		
		int months = 2;
		int randomNum = _rand.nextInt(3000 - 365 + 1) + 365;
		test4(randomNum, _prod1, months);
		test4(1, _prod2, months);
		
		TimeSource.instance().setTime(_now);
		List<List<String>> data = runReport(months);
		testAssert(1, _prod1, data, months);
		testAssert(2, _prod2, data, months);
	}
	
	private void test4(int randomNum, Product prod, int months) throws ModelException, IOException
	{
		Calendar longTimeBefore = (Calendar) _now.clone();
		longTimeBefore.add(Calendar.MONTH, -1*months);
		longTimeBefore.add(Calendar.DAY_OF_MONTH, -1*randomNum);
		randomNum = _rand.nextInt(27) + 1;
		Calendar middle = (Calendar) _now.clone();
		middle.add(Calendar.DAY_OF_MONTH, -1*randomNum);
		TimeSource.instance().setTime(longTimeBefore);
		_pFacade.addProduct(prod);
		_iFacade.addItem(prod, _su);
		_iFacade.addItem(prod, _su);
		
		TimeSource.instance().setTime(middle);
		_iFacade.addItem(prod, _su);
	}
	
	@Test
	///////////////////////////////////////////////////////////////
	// 10e. Test for a product that was added once and used once 
	//         during the period (same item)
	///////////////////////////////////////////////////////////////
	public void test10ePart1() throws ModelException, IOException
	{
		int months = 1;
		Calendar beforeReport = (Calendar)_now.clone();
		beforeReport.add(Calendar.MONTH, -1*months);
		beforeReport.add(Calendar.DAY_OF_MONTH, -1);
		
		TimeSource.instance().setTime(beforeReport);
		_pFacade.addProduct(_prod1);
		_iFacade.addItem(_prod1, _su);
		test10AddRemove(_prod1, null);
		
		_pFacade.addProduct(_prod2);
		test10AddRemove(_prod2, null);
		
		TimeSource.instance().setTime(_now);
		List<List<String>> data = runReport(months);
		testAssert(1, _prod1, data, months);
		testAssert(2, _prod2, data, months);
	}
	
	private void test10AddRemove(Product prod, Item item) throws ModelException
	{
		DateUtils.setToMidnight(_now);
		Calendar during1Report = (Calendar)_now.clone();
		Calendar during2Report = (Calendar)_now.clone();
		int rand = _rand.nextInt(28) + 1;
		during1Report.add(Calendar.DAY_OF_MONTH, -1*rand);
		rand = _rand.nextInt(28) + 1;
		during2Report.add(Calendar.DAY_OF_MONTH, -1*rand);
		while (during1Report.equals(during2Report))
		{
			rand = _rand.nextInt(28) + 1;
			during2Report.add(Calendar.DAY_OF_MONTH, -1*rand);
		}
		if (during1Report.after(during2Report))
		{
			Calendar temp = (Calendar) during2Report.clone();
			during2Report = (Calendar) during1Report.clone();
			during1Report = (Calendar) temp.clone();
		}
		
		TimeSource.instance().setTime(during1Report);
		
		if (item == null)
		{
			item = _iFacade.addItem(prod, _su);
		}
		else
		{
			Item temp = _iFacade.addItem(prod, _su);
		}
		
		Calendar entry = item.getEntryDate();
		DateUtils.setToMidnight(entry);
		if (during2Report.before(item.getEntryDate()))
		{
			during2Report.setTime(entry.getTime());
		}
		TimeSource.instance().setTime(during2Report);
		_iFacade.removeItem(item);
	}
	
	@Test
	////////////////////////////////////////////////////
	// 10f. Test for a product that was added once and 
	//       used once during the period (different items)
	/////////////////////////////////////////////////////
	public void test10f() throws ModelException, IOException
	{
		int months = 1;
		Calendar beforeReport = (Calendar)_now.clone();
		beforeReport.add(Calendar.MONTH, -1*months);
		beforeReport.add(Calendar.DAY_OF_MONTH, -1);
		
		TimeSource.instance().setTime(beforeReport);
		_pFacade.addProduct(_prod1);
		Item item = _iFacade.addItem(_prod1, _su);
		
		test10AddRemove(_prod1, item);
		
		TimeSource.instance().setTime(_now);
		List<List<String>> data = runReport(months);
		testAssert(1, _prod1, data, months);
	}
	
	@Test
	//////////////////////////////////////////////////
	// 10g. Test for a product that was added multiple 
	//			times and used once during the period
	//////////////////////////////////////////////////
	public void test10g() throws ModelException, IOException
	{
		int months = 1;
		_pFacade.addProduct(_prod1);
		int numToAdd = _rand.nextInt(10) + 2;
		int removeThis = _rand.nextInt(numToAdd);
		Item removeItem = null;
		for (int i = 0; i < numToAdd; i++)
		{
			int rand = _rand.nextInt(28) + 1;
			Calendar duringReport = (Calendar)_now.clone();
			duringReport.add(Calendar.DAY_OF_MONTH, -1*rand);
			TimeSource.instance().setTime(duringReport);
			Item item =_iFacade.addItem(_prod1, _su);
			if (i == removeThis)
				removeItem = item;
		}
		test10AddRemove(_prod1, removeItem);

		TimeSource.instance().setTime(_now);
		List<List<String>> data = runReport(months);
		testAssert(1, _prod1, data, months);
	}
	
	@Test
	///////////////////////////////////////////////////
	// 10h. Test for a product that was added once and 
	//         	used multiple times during the period
	///////////////////////////////////////////////////
	public void test10h() throws ModelException, IOException
	{
		ArrayList<Item> items = new ArrayList<Item>();
		int months = 1;
		_pFacade.addProduct(_prod1);
		
		int rand = _rand.nextInt(15) + 3;
		
		for (int i = 0; i < rand; i++)
		{
			int subtract = _rand.nextInt(365) + 1;
			Calendar date = (Calendar)_now.clone();
			date.add(Calendar.MONTH, -1*months);
			date.add(Calendar.DAY_OF_MONTH, -1* subtract);
			TimeSource.instance().setTime(date);
			items.add(_iFacade.addItem(_prod1, _su));
		}
		
		int num = _rand.nextInt(28) + 1;
		Calendar date = (Calendar)_now.clone();
		date.add(Calendar.DAY_OF_MONTH, -1* num);
		TimeSource.instance().setTime(date);
		items.add(_iFacade.addItem(_prod1, _su));
		
		int numToRemove = _rand.nextInt(rand+1) + 2;
		
		for (int i = 0; i < numToRemove; i++)
		{
			int index = _rand.nextInt(items.size());
			_iFacade.removeItem(items.get(index));
		}
		
		TimeSource.instance().setTime(_now);
		List<List<String>> data = runReport(months);
		testAssert(1, _prod1, data, months);
	}
	private void testAssert(int row, Product prod,List<List<String>> data, int months) 
			throws NotFoundException 
	{
		Calendar start = (Calendar)_now.clone();
		start.add(Calendar.MONTH, -1*months);
		OracleForProdStatReport myOracle = new OracleForProdStatReport(prod, start, _now);
		
		List<String> rowData = data.get(row);
		assertEquals(rowData.get(0), prod.getDescription());
		assertEquals(rowData.get(1), prod.getBarcode().toString());
		assertEquals(rowData.get(2), prod.getSize().toString());
		if (prod.getThreeMonthSupply() != 0)
			assertEquals(Integer.parseInt(rowData.get(3)), prod.getThreeMonthSupply());
		else
			assertEquals(rowData.get(3), "");
			
		int curSupply = myOracle.getCurrentSupply();
		float avgSupply = myOracle.getSupplyAvg();
		assertEquals("supply cur/avg supply", curSupply + " / " + roundFloat(avgSupply), rowData.get(4));
		
		int min = myOracle.getSupplyMin();
		int max = myOracle.getSupplyMax();
		assertEquals("supply min/max supply", min + " / " + max, rowData.get(5));
		
		int used = myOracle.getSupplyUsed();
		int added = myOracle.getSupplyAdded();
		assertEquals("supply used/added", used + " / " + added, rowData.get(6));
		
		if (prod.getShelfLife() == 0)
			assertEquals("shelf life", "", rowData.get(7));
		else
			assertEquals("shelf life", prod.getShelfLife() + " months", rowData.get(7));
		
		float usedAvg = myOracle.getUsedAvg();
		int usedMax = myOracle.getUsedMax();
		assertEquals("used avg/max", roundFloat(usedAvg) + " days /\n" + usedMax + " days", 
				rowData.get(8));
		
		float currAvg = myOracle.getCurrAvg();
		int currMax = myOracle.getCurrMax();
		assertEquals("currAge avg/max", roundFloat(currAvg) + " days /\n" + currMax + " days", 
				rowData.get(9));
	}

	private List<List<String>> runReport(int months) throws IOException
	{
		TestBuilder builder = new TestBuilder("Title of report");
    	TimeSource.instance().setTime(_now);
    	ProductStatsVisitor visitor = new ProductStatsVisitor(builder, months);
    	ProductManager.instance().accept(visitor);
    	builder.print();
    	return builder.getData();
	}
	
	private String roundFloat(Float f)
	{
		DecimalFormat df = new DecimalFormat("#0.#");
		return  df.format(f);
	}
}
